#import <QuartzCore/QuartzCore.h>
#import "ALTimelineView.h"
#import "NSDate+SMRAdditions.h"
#import "NSObject+BlockObservation.h"
#import "NSArray+SMRAdditions.h"
#import "ALCanvasPage.h"
#import "ALTimelineElement.h"
#import "ALLayoutSegment.h"
#import "ALRubberBandView.h"
#import "ALScrollFixedElementAnimation.h"
#import "ALTimelineScale.h"
#import "ALTimelineCanvas.h"


typedef enum {
	ALSelectionDirectionLeft = -1,
	ALSelectionDirectionRight = 1
} ALSelectionDirection;


#pragma mark Constants
const CGFloat ALScrollAnimationDuration = 0.15f;
const CGFloat ALTimelineScaleHeight = 50.0f;


#pragma mark Bindings keys
NSString* const ALTimelineContentBinding = @"timelineContent";
NSString* const ALTimelineDateKeyPathOption = @"ALTimelineDateKeyPathOption";

#pragma mark Notification keys
NSString* const ALTimelineElementsDidChangeNotification = @"ALTimelineElementsDidChangeNotification";
NSString* const ALTimelineInsertedEntriesKey = @"ALTimelineInsertedEntriesKey";
NSString* const ALTimelineRemovedEntriesKey = @"ALTimelineRemovedEntriesKey";
NSString* const ALTimelineMovedEntriesKey = @"ALTimelineMovedEntriesKey";

#pragma mark Encoding keys
static NSString* const ALTimelineViewDelegateKey = @"ALTimelineViewDelegateKey";
static NSString* const ALTimelineViewElementPrototypeKey = @"ALTimelineViewElementPrototypeKey";
static NSString* const ALTimelineViewDateKeyPathKey = @"ALTimelineViewDateKeyPathKey";
//static NSString* const ALTimelineViewSortDescriptorsKey = @"ALTimelineViewSortDescriptorsKey";

#pragma mark Other keys
static NSString* const ALTimelineElementViewToRemove = @"ALTimelineElementViewToRemove";
static NSString* const ALTimelineFrameOriginKey = @"frameOrigin";
static NSString* const ALTimelineRepresentedObjectKey = @"representedObject";
static NSString* const ALTimelineSelectedKey = @"selected";
//static NSString* const ALTimelineDefaultDateKeyPath = @"date";
static NSString* const ALTimelineMinimumSizeKey = @"minimumSize";
static NSString* const ALTimelineSelectedElementsKey = @"selectedElements";


#pragma mark -
#pragma mark Class globals


#pragma mark -
@interface ALTimelineView ()
@property (nonatomic, retain) NSArray *elementObjects;
@property (nonatomic, retain, readwrite) NSArray *timelineElements;
@property (nonatomic, retain, readwrite) NSArray *selectedElements;
@property (nonatomic, retain) ALTimelineElement *lastAnchorSelection;
@property (nonatomic, retain) ALTimelineElement *lastExtendSelection;
@property (nonatomic, assign) NSUInteger totalDays;
@property (nonatomic, retain, readwrite) ALTimelineElement *editingElement;
@end

#pragma mark -
@interface ALTimelineView (ALTimelineViewPrivateMethods)
-(void)initializeTimelineView_;
-(BOOL)isLayoutRequired_;
-(NSRect)visibleRect_;
//-(NSArray *)allSortDescriptors_;
-(void)loadElementsWithObjects_:(NSArray *)objects;
-(void)postElementsDidChangeNotificationWithInserted_:(NSArray *)inserted withRemoved:(NSArray *)removed withMoved:(NSArray *)moved;
-(NSUInteger)indexForElementObject_:(id)object inElements:(NSArray *)elements;
-(NSDate *)dateForRepresentedObject_:(id)object;
-(void)refreshDaysFromFirst_;
-(ALTimelineElement *)elementForRepresentedObject:(id)object;
-(BOOL)delegateConcursToSelection:(NSArray *)elementsToSelect;
-(BOOL)delegateConcursToDeselection:(NSArray *)elementsToDeselect;
-(void)selectWithDirection_:(ALSelectionDirection)selectionDirection andModify_:(BOOL)shouldModifySelection;
-(ALTimelineElement *)closestSelectedElementTo_:(ALTimelineElement *)compareElement;
-(BOOL)selectElements_:(NSArray *)elements extendSelection:(BOOL)extendSelection resetAnchor:(BOOL)shouldResetAnchor;
-(NSSize)requiredSize_;
-(NSSize)timelineSizeForCanvasSize_:(NSSize)canvasSize;
-(void)updateFrame_;
-(void)startObservingDateForElement_:(ALTimelineElement *)element;
@end


#pragma mark -
@implementation ALTimelineView

@synthesize canvas;
@synthesize scale;
@synthesize delegate;
@synthesize dragDropDelegate;
@synthesize elementPrototype;
@synthesize timelineElements;
@synthesize dateKeyPath;
//@synthesize elementSortDescriptors;
@synthesize elementObjects;
@synthesize selectedElements;
@synthesize lastAnchorSelection;
@synthesize lastExtendSelection;
@synthesize totalDays;
@synthesize editingElement;


#pragma mark -
#pragma mark Initialization

+(void)initialize {
	if (self == [ALTimelineView class])
		[self exposeBinding:ALTimelineContentBinding];
}

-(id)initWithFrame:(NSRect)frameRect {
	if (!(self = [super initWithFrame:frameRect]))
		return nil;
	[self initializeTimelineView_];
	return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder {
	if (!(self = [super initWithCoder:aDecoder]))
		return nil;
	[self initializeTimelineView_];
	[self setDelegate:[aDecoder decodeObjectForKey:ALTimelineViewDelegateKey]];
	[self setElementPrototype:[aDecoder decodeObjectForKey:ALTimelineViewElementPrototypeKey]];
	[self setDateKeyPath:[aDecoder decodeObjectForKey:ALTimelineViewDateKeyPathKey]];
//	[self setElementSortDescriptors:[aDecoder decodeObjectForKey:ALTimelineViewSortDescriptorsKey]];
	return self;
}

-(void)initializeTimelineView_ {
//	[self setDateKeyPath:ALTimelineDefaultDateKeyPath];
	[self setTimelineElements:[NSArray array]];
	ALTimelineCanvas *aCanvas = [[[ALTimelineCanvas alloc] initWithFrame:NSZeroRect] autorelease];
	[aCanvas setTimeline:self];
	[self setCanvas:aCanvas];
	[self addSubview:aCanvas];
	ALTimelineScale *aScale = [[[ALTimelineScale alloc] initWithFrame:NSZeroRect] autorelease];
	[aScale setTimeline:self];
	[self setScale:aScale];
	[self addSubview:aScale];
	AMBlockTask sizeTask = ^(id object, NSDictionary *change) {
		[self updateFrame_];
	};
	[aCanvas addObserverForKeyPath:ALTimelineMinimumSizeKey task:sizeTask];
}

-(void)encodeWithCoder:(NSCoder *)aCoder {
	[super encodeWithCoder:aCoder];
	[aCoder encodeConditionalObject:[self delegate] forKey:ALTimelineViewDelegateKey];
	[aCoder encodeConditionalObject:[self elementPrototype] forKey:ALTimelineViewElementPrototypeKey];
	[aCoder encodeObject:[self dateKeyPath] forKey:ALTimelineViewDateKeyPathKey];
//	[aCoder encodeObject:[self elementSortDescriptors] forKey:ALTimelineViewSortDescriptorsKey];
}

-(void)viewDidMoveToWindow {
	[self tile];
}


#pragma mark -
#pragma mark View attributes

//-(BOOL)acceptsFirstResponder {
//	return YES;
//}


#pragma mark -
#pragma mark Data 

-(id)timelineContent {
	NSDictionary *bindingInfo = [self infoForBinding:ALTimelineContentBinding];
	id object = [bindingInfo valueForKey:NSObservedObjectKey];
	NSString *keyPath = [bindingInfo valueForKey:NSObservedKeyPathKey];
	id contentObject = [object valueForKeyPath:keyPath];
	if ([contentObject isKindOfClass:[NSArray class]])
		return [self elementObjects];
	if ([contentObject isKindOfClass:[NSSet class]])
		return [NSSet setWithArray:[self elementObjects]];
	return nil;
}

-(void)setTimelineContent:(id)contentObject {
	BOOL contentIsArray = [contentObject isKindOfClass:[NSArray class]];
	BOOL contentIsSet = [contentObject isKindOfClass:[NSSet class]];
	NSAssert((contentIsSet || contentIsArray), ([NSString stringWithFormat:@"ALTimelineView: Attempting to bind timeline to object of type %@, but only objects of type NSArray or NSSet are supported.", [contentObject className]]));
	if (contentIsArray)
		[self setElementObjects:[NSArray arrayWithArray:contentObject]];
	else if (contentIsSet)
		[self setElementObjects:[contentObject allObjects]];
	else 
		return;
	[self rearrangeObjects];
}

-(NSArray *)arrangeObjects:(NSArray *)objects {
//	return [objects sortedArrayUsingDescriptors:[self allSortDescriptors_]];
	NSArray *sortedObjects = [objects sortedArrayUsingComparator:^(id object1, id object2) {
		NSDate *date1 = [self dateForRepresentedObject_:object1];
		NSDate *date2 = [self dateForRepresentedObject_:object2];
		return [date1 compare:date2];
	}];
	return sortedObjects;
}

-(void)rearrangeObjects {
	NSArray *sortedObjects = [self arrangeObjects:[self elementObjects]];
	[self loadElementsWithObjects_:sortedObjects];
}

-(void)loadElementsWithObjects_:(NSArray *)objects {
	NSMutableArray *oldElements = [[self timelineElements] mutableCopy];
	NSMutableArray *newElements = [NSMutableArray arrayWithCapacity:[objects count]];
	NSMutableArray *inserted = [NSMutableArray array];
	NSMutableArray *moved = [NSMutableArray array];
	NSMutableIndexSet *removalIndexSet = [NSMutableIndexSet indexSet];
	
	NSUInteger elementIndex;
	for (elementIndex = 0; elementIndex < [objects count]; elementIndex++) {
		id elementObject = [objects objectAtIndex:elementIndex];
		NSUInteger oldElementIndex;
		ALTimelineElement *element = [self elementWithRepresentedObject:elementObject returnIndex:&oldElementIndex];
		if (!element) {
			element = [self elementForRepresentedObject:elementObject];
			[inserted addObject:element];
		}
		else if (elementIndex != oldElementIndex)
			[moved addObject:element];
		[newElements addObject:element];
		if (oldElementIndex != NSNotFound)
			[removalIndexSet addIndex:oldElementIndex];
	}
	[oldElements removeObjectsAtIndexes:removalIndexSet];
//	[self setRemovedElements:oldElements];
	for (ALTimelineElement *removedElement in oldElements) 
		[removedElement timelineElementWasRemovedFromTimeline:self];
//	[self setAddedElements:inserted];
	[self setTimelineElements:newElements];
	[self refreshDaysFromFirst_];
//	[self setNeedsLayout:YES];
	for (ALTimelineElement *insertedElement in inserted) 
		[insertedElement timelineElementWasAddedToTimeline];
	[self postElementsDidChangeNotificationWithInserted_:inserted withRemoved:oldElements withMoved:moved];
	[oldElements autorelease];
}

-(void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options {
	if ([binding isEqualToString:ALTimelineContentBinding]) {
		NSString *datePath = [options valueForKey:ALTimelineDateKeyPathOption];
		if (datePath)
			[self setDateKeyPath:datePath];
	}
	[super bind:binding toObject:observable withKeyPath:keyPath options:options];	
}


#pragma mark -
#pragma mark Finding and sorting elements

-(ALTimelineElement *)elementWithRepresentedObject:(id)object returnIndex:(NSUInteger *)elementIndex {
	NSUInteger foundIndex = [self indexForElementObject_:object inElements:[self timelineElements]];
	if (elementIndex != NULL)
		*elementIndex = foundIndex;
	if (foundIndex == NSNotFound)
		return nil;
	return [[self timelineElements] objectAtIndex:foundIndex];
}

-(ALTimelineElement *)elementWithView:(NSView *)aView {
	NSRect viewFrame = [aView frame];
	ALTimelineElement *element = [[self canvas] elementAtPoint:viewFrame.origin];
	if ([element view] == aView)
		return element;
	// TODO: THIS CAN BE IMPROVED BY GETTING THE VIEW'S ORIGIN IN TIMELINE VIEW COORDINATES, FINDING THE PAGE THAT IT'S IN AND SEARCHING THERE
	for (ALTimelineElement *currentElement in [self timelineElements]) {
		NSView *elementView = [currentElement view];
		if ([aView isDescendantOf:elementView])
			return currentElement;
	}
	return nil;
}

// Binary search for element object by its associated date
-(NSUInteger)indexForElementObject_:(id)object inElements:(NSArray *)elements {
	NSUInteger count = [elements count];
	if (count == 0)
		return NSNotFound;
	NSUInteger middle = floor(count/2);
	ALTimelineElement *middleElement = [elements objectAtIndex:middle];
	id middleObject = [middleElement representedObject];
	if (middleObject == object) 
		return middle;
	if (count <= 1) 
		return NSNotFound;
	NSDate *objectDate = [self dateForRepresentedObject_:object];
	NSDate *middleDate = [middleElement date];
	if ([objectDate earlierDate:middleDate] == objectDate) {
		NSRange leftRange = NSMakeRange(0, middle);
		return [self indexForElementObject_:object inElements:[elements subarrayWithRange:leftRange]];
	}
	if (middle == count - 1) 
		return NSNotFound;
	NSRange rightRange = NSMakeRange(middle + 1, count - middle - 1);
	NSUInteger rightReturn = [self indexForElementObject_:object inElements:[elements subarrayWithRange:rightRange]];
	if (rightReturn != NSNotFound)
		rightReturn += middle + 1;
	return rightReturn;
}

//-(NSArray *)allSortDescriptors_ {
//	if (![self dateKeyPath])
//		return [self elementSortDescriptors];
//	NSArray *dateDescriptor = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:[self dateKeyPath] ascending:YES]];
//	if (![self elementSortDescriptors])
//		return dateDescriptor;
//	return [dateDescriptor arrayByAddingObjectsFromArray:[self elementSortDescriptors]];
//}

//-(NSArray *)sortElements_:(NSArray *)elementsToSort {
//	return [elementsToSort sortedArrayUsingDescriptors:[self allSortDescriptors_]];
//}


#pragma mark -
#pragma mark Utilities

-(void)refreshDaysFromFirst_ {
	if ([[self timelineElements] count] == 0)
		return;
	ALTimelineElement *firstElement = [[self timelineElements] objectAtIndex:0];
	NSDate *firstDate = [firstElement date];
	[firstElement setDaysFromFirst:0];
	for (ALTimelineElement *element in [self timelineElements]) {
		NSDate *elementDate = [element date];
		NSInteger deltaDays = [elementDate smrDaysAfterDate:firstDate];
		[element setDaysFromFirst:deltaDays];
	}
	ALTimelineElement *lastElement = [[self timelineElements] lastObject];
	[self setTotalDays:[lastElement daysFromFirst]];
}

-(NSDate *)dateForRepresentedObject_:(id)object {
	NSDate *delegateDate = [[self delegate] timeline:self dateForRepresentedObject:object];
	if (delegateDate)
		return delegateDate;
	if (![self dateKeyPath])
		return nil;
	return [object valueForKeyPath:[self dateKeyPath]];
}

-(ALTimelineElement *)elementForRepresentedObject:(id)object {
	ALTimelineElement *newElement = nil;
	if ([[self delegate] respondsToSelector:@selector(timeline:elementForRepresentedObject:)]) 
		newElement = [[self delegate] timeline:self elementForRepresentedObject:object];
	if (!newElement) {
		NSAssert([self elementPrototype] != nil, @"ALTimelineView: Unable to create new ALTimelineElement.  No prototype provided.");
		newElement = [[[self elementPrototype] copy] autorelease];
	}
	
	[newElement setRepresentedObject:object];
	[newElement setTimeline:self];
	[self startObservingDateForElement_:newElement];
	return newElement;
}

-(void)startObservingDateForElement_:(ALTimelineElement *)element {
	if (![self dateKeyPath])
		return;
	
	AMBlockTask dateTask = ^(id object, NSDictionary *change) {
		[self rearrangeObjects];
	};
	NSString *keyPath = [NSString stringWithFormat:@"%@.%@", ALTimelineRepresentedObjectKey, [self dateKeyPath]];
	[element addObserverForKeyPath:keyPath task:dateTask];
}


#pragma mark -
#pragma mark Geometry

-(void)scrollElementToVisible:(ALTimelineElement *)element {
	NSScrollView *enclosingScrollView = [self enclosingScrollView];
	if (!enclosingScrollView)
		return;
	NSRect elementRect = [element currentPosition];
	NSRect visibleCanvasRect = [[self canvas] visibleRect];
	if (NSContainsRect(visibleCanvasRect, elementRect))
		return;
	NSRect documentRect = [[enclosingScrollView documentView] convertRect:elementRect fromView:[self canvas]];
	ALScrollFixedElementAnimation *scrollAnimation = [[ALScrollFixedElementAnimation alloc] initWithDuration:ALScrollAnimationDuration animationCurve:NSAnimationEaseInOut];
	[scrollAnimation setAnimationBlockingMode:NSAnimationNonblocking];
	[scrollAnimation animateScrollView:enclosingScrollView toRect:documentRect];
	[scrollAnimation startAnimation];
}


#pragma mark -
#pragma mark Sizing

-(void)resizeWithOldSuperviewSize:(NSSize)oldBoundsSize {
	NSScrollView *enclosingScrollView = [self enclosingScrollView];
	if (!enclosingScrollView) 
		[super resizeWithOldSuperviewSize:oldBoundsSize];
	else 
		[self updateFrame_];
}

-(void)updateFrame_ {
	NSRect frame = [self frame];
	NSSize requiredSize = [self requiredSize_];
	frame.size = requiredSize;
	[self setFrame:frame];
	[self tile];
}

-(NSSize)requiredSize_ {
	NSScrollView *enclosingScrollView = [self enclosingScrollView];
	NSSize visibleSize = enclosingScrollView ? [enclosingScrollView documentVisibleRect].size : [self frame].size;
	NSSize minimumSize = [self timelineSizeForCanvasSize_:[[self canvas] minimumSize]];
	NSSize requiredSize = NSMakeSize(MAX(visibleSize.width, minimumSize.width), MAX(visibleSize.height, minimumSize.height));
	return requiredSize;
}

-(NSSize)timelineSizeForCanvasSize_:(NSSize)canvasSize {
	NSSize timelineSize = canvasSize;
	timelineSize.height += ALTimelineScaleHeight;
	return timelineSize;
}

-(void)tile {
	NSRect canvasRect = [self canvasRectForBounds:[self bounds]];
	[[self canvas] setFrame:canvasRect];
	NSRect scaleRect = [self scaleRectForBounds:[self bounds]];
	[[self scale] setFrame:scaleRect];
}

-(NSRect)canvasRectForBounds:(NSRect)bounds {
	return NSMakeRect(bounds.origin.x, ALTimelineScaleHeight, bounds.size.width, bounds.size.height - ALTimelineScaleHeight);
}

-(NSRect)scaleRectForBounds:(NSRect)bounds {
	return NSMakeRect(bounds.origin.x, bounds.origin.y, bounds.size.width, ALTimelineScaleHeight);
}

-(NSRect)visibleRect_ {
	NSScrollView *enclosingScrollView = [self enclosingScrollView];
	return enclosingScrollView ? [enclosingScrollView documentVisibleRect] : [self visibleRect];
}


#pragma mark -
#pragma mark Notifications

-(void)postElementsDidChangeNotificationWithInserted_:(NSArray *)inserted withRemoved:(NSArray *)removed withMoved:(NSArray *)moved {
	NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:inserted, ALTimelineInsertedEntriesKey, removed, ALTimelineRemovedEntriesKey, moved, ALTimelineMovedEntriesKey, nil];
	NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
	[noteCenter postNotificationName:ALTimelineElementsDidChangeNotification object:self userInfo:userInfo];
}


//#pragma mark -
//#pragma mark Drawing
//
//-(void)drawRect:(NSRect)dirtyRect {
//	[[NSColor yellowColor] setFill];
//	NSRectFill([self bounds]);
//}


#pragma mark -
#pragma mark Selection

+(NSSet *)keyPathsForValuesAffectingSelectedObjects {
	return [NSSet setWithObject:ALTimelineSelectedElementsKey];
}

-(NSArray *)selectedObjects {
	return [[self selectedElements] valueForKey:ALTimelineRepresentedObjectKey];
}

-(BOOL)selectElement:(ALTimelineElement *)element extendSelection:(BOOL)extendSelection {
	if (!element)
		return NO;
	return [self selectElements:[NSArray arrayWithObject:element] extendSelection:extendSelection];
}

-(BOOL)selectElements:(NSArray *)elements extendSelection:(BOOL)extendSelection {
	return [self selectElements_:elements extendSelection:extendSelection resetAnchor:YES];
}

-(BOOL)selectNextElementAndExtendSelection:(BOOL)shouldExtend {
	[self selectWithDirection_:ALSelectionDirectionRight andModify_:shouldExtend];
    return YES;
}

-(BOOL)selectPreviousElementAndExtendSelection:(BOOL)shouldExtend {
	[self selectWithDirection_:ALSelectionDirectionLeft andModify_:shouldExtend];
    return YES;
}

-(BOOL)selectElements_:(NSArray *)elements extendSelection:(BOOL)extendSelection resetAnchor:(BOOL)shouldResetAnchor {
	if ([elements count] == 0 && !extendSelection) 
		return [self deselectAllElements];
	if ([[self selectedElements] smrIsEquivalentToArray:elements])
		return YES;
	if (![self delegateConcursToSelection:elements])
		return NO;
	NSArray *deselectElements = [[self selectedElements] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT self IN %@", elements]];
	if (!extendSelection && [deselectElements count] > 0 && ![self deselectElements:deselectElements])
		return NO;
	[self stopEditing];
	NSArray *unselected = [elements filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT self IN %@", [self selectedElements]]];
	[unselected setValue:[NSNumber numberWithBool:YES] forKey:ALTimelineSelectedKey];
	NSMutableArray *newSelected = [NSMutableArray arrayWithCapacity:[unselected count] + [[self selectedElements] count]];
	if (extendSelection) {
		[newSelected addObjectsFromArray:[self selectedElements]];
		[newSelected addObjectsFromArray:unselected];
	}
	else 
		[newSelected addObjectsFromArray:elements];
	[self setSelectedElements:newSelected];
	if (shouldResetAnchor) {
		[self setLastAnchorSelection:[elements objectAtIndex:0]];
		[self setLastExtendSelection:nil];
	}
	return YES;
}

-(BOOL)selectAllElements {
	return [self selectElements:[self timelineElements] extendSelection:NO];
}

-(BOOL)deselectElement:(ALTimelineElement *)anElement {
	if (!anElement)
		return NO;
	return [self deselectElements:[NSArray arrayWithObject:anElement]];
}

-(BOOL)deselectElements:(NSArray *)elements {
	if ([elements count] == 0)
		return YES;
	if (![[self selectedElements] smrContainsObjectsInArray:elements] || ![self delegateConcursToDeselection:elements])
		return NO;
	[self stopEditing];
	[elements setValue:[NSNumber numberWithBool:NO] forKey:ALTimelineSelectedKey];
	NSArray *newSelected = [[self selectedElements] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT self IN %@", elements]];
	[self setSelectedElements:newSelected];
	if ([elements containsObject:[self lastAnchorSelection]]) {
		ALTimelineElement *closestSelectedElement = [self closestSelectedElementTo_:[self lastAnchorSelection]];
		[self setLastAnchorSelection:closestSelectedElement];
		[self setLastExtendSelection:nil];
	}
	return YES;
}

-(BOOL)deselectAllElements {
	return [self deselectElements:[self selectedElements]];
}

-(BOOL)delegateConcursToSelection:(NSArray *)elementsToSelect {
	if (![[self delegate] respondsToSelector:@selector(timeline:shouldSelectElement:)])
		return YES;
	for (ALTimelineElement *element in elementsToSelect) {
		if (![[self delegate] timeline:self shouldSelectElement:element]) 
			return NO;	
	}
	return YES;
}

-(BOOL)delegateConcursToDeselection:(NSArray *)elementsToDeselect {
	if (![[self delegate] respondsToSelector:@selector(timeline:shouldDeselectElement:)])
		return YES;
	for (ALTimelineElement *element in elementsToDeselect) {
		if (![[self delegate] timeline:self shouldDeselectElement:element]) 
			return NO;	
	}
	return YES;
}

-(ALTimelineElement *)closestSelectedElementTo_:(ALTimelineElement *)compareElement {
	if ([[self selectedElements] count] == 0)
		return nil;
	NSUInteger compareDays = [compareElement daysFromFirst];
	NSUInteger minimumDelta = NSUIntegerMax;
	ALTimelineElement *closestElement = nil;
	for (ALTimelineElement *selectedElement in [self selectedElements]) {
		NSUInteger selectedDays = [selectedElement daysFromFirst];
		NSUInteger deltaDays = (NSUInteger) fabs(compareDays - selectedDays);
		if (deltaDays < minimumDelta) {
			minimumDelta = deltaDays;
			closestElement = selectedElement;
		}
		else if (deltaDays == minimumDelta) {
			NSUInteger compareIndex = [self indexForElementObject_:[compareElement representedObject] inElements:[self timelineElements]];
			NSUInteger closestIndex = [self indexForElementObject_:[closestElement representedObject] inElements:[self timelineElements]];
			NSUInteger selectedIndex = [self indexForElementObject_:[selectedElement representedObject] inElements:[self timelineElements]];
			if (fabs(compareIndex - selectedIndex) < fabs(compareIndex - closestIndex))
				closestElement = selectedElement;
		}
	}
	return closestElement;
}

-(void)extendSelectionFromAnchorToElement_:(ALTimelineElement *)element {
	NSUInteger lastAnchorIndex = [self indexForElementObject_:[[self lastAnchorSelection] representedObject] inElements:[self timelineElements]];
	NSUInteger elementIndex = [self indexForElementObject_:[element representedObject] inElements:[self timelineElements]];
	NSRange selectionRange = NSMakeRange(MAX(lastAnchorIndex, elementIndex), fabs(lastAnchorIndex - elementIndex));
	NSArray *selectElements = [[self timelineElements] subarrayWithRange:selectionRange];
	[self selectElements:selectElements extendSelection:YES];
}

-(void)selectWithDirection_:(ALSelectionDirection)selectionDirection andModify_:(BOOL)shouldModifySelection {
	if ([[self timelineElements] count] == 0) {
		NSBeep();
		return;
	}
	if ([[self selectedElements] count] == 0) {
		ALTimelineElement *endElement = selectionDirection == ALSelectionDirectionLeft ? [[self timelineElements] lastObject] : [[self timelineElements] objectAtIndex:0];
		[self selectElement:endElement extendSelection:NO];
		[self scrollElementToVisible:endElement];
		return;
	}
	ALTimelineElement *lastExtend = [self lastExtendSelection];
	if (!lastExtend || ![[self timelineElements] containsObject:lastExtend])
		lastExtend = [self lastAnchorSelection];
	
	NSUInteger lastExtendIndex = [self indexForElementObject_:[lastExtend representedObject] inElements:[self timelineElements]];
	NSUInteger lastAnchorIndex = [self indexForElementObject_:[[self lastAnchorSelection] representedObject] inElements:[self timelineElements]];
	if (!shouldModifySelection) {
		if ((lastExtendIndex == 0 && selectionDirection == ALSelectionDirectionLeft) || (lastExtendIndex == [[self timelineElements] count] - 1 && selectionDirection == ALSelectionDirectionRight))
			return;
		lastExtendIndex += selectionDirection;
		ALTimelineElement *elementToSelect = [[self timelineElements] objectAtIndex:lastExtendIndex];
		[self selectElement:elementToSelect extendSelection:NO];
		[self scrollElementToVisible:elementToSelect];
		return;
	}
	BOOL addingToExtent = (selectionDirection == ALSelectionDirectionLeft && lastExtendIndex <= lastAnchorIndex) || (selectionDirection == ALSelectionDirectionRight && lastExtendIndex >= lastAnchorIndex);
	if (!addingToExtent) {
		if (![self deselectElement:lastExtend])
			return;
		lastExtendIndex += selectionDirection;
		ALTimelineElement *newExtend = [[self timelineElements] objectAtIndex:lastExtendIndex];
		[self setLastExtendSelection:newExtend];
		[self scrollElementToVisible:newExtend];
		return;
	}
	ALTimelineElement *nextElement = nil;
	for (; lastExtendIndex < [[self timelineElements] count]; lastExtendIndex += selectionDirection) {
		nextElement = [[self timelineElements] objectAtIndex:lastExtendIndex];
		if ([nextElement selected]) {
			[self setLastExtendSelection:nextElement];
			continue;
		}
		if (![self selectElements_:[NSArray arrayWithObject:nextElement] extendSelection:YES resetAnchor:NO])
			return;
		[self scrollElementToVisible:nextElement];
		[self setLastExtendSelection:nextElement];
		break;
	}
}


#pragma mark -
#pragma mark Event handling

-(NSView *)hitTest:(NSPoint)aPoint {
	NSView *superResult = [super hitTest:aPoint];
	NSPoint scalePoint = [[self scale] convertPoint:aPoint fromView:[self superview]];
	if ([[self scale] mouse:scalePoint inRect:[[self scale] bounds]]) 
		return superResult;
	
	if (![[self delegate] respondsToSelector:@selector(timeline:willForwardHitTestAtPoint:toSubview:inElement:)]) 
		return [self canvas];

	NSPoint canvasPoint = [[self canvas] convertPoint:aPoint fromView:[self superview]];
	ALTimelineElement *hitElement = [[self canvas] elementAtPoint:canvasPoint];
	NSView *delegateChosenView = [[self delegate] timeline:self willForwardHitTestAtPoint:aPoint toSubview:superResult inElement:hitElement];
	if (!delegateChosenView)
		delegateChosenView = [self canvas];
	return delegateChosenView;
}


#pragma mark -
#pragma mark Editing

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
	if (![keyPath isEqualToString:@"firstResponder"])
		return;
	NSResponder *firstResponder = [[self window] firstResponder];
	// If new firstResponder is a subview of the editing view, then editing is still underway so we return
	if ([firstResponder isKindOfClass:[NSView class]] && [(NSView *)firstResponder isDescendantOf:[[self editingElement] view]])
		return;
	NSResponder *previousFirstResponder = [change valueForKey:NSKeyValueChangeOldKey];
	// If previous firstResponder was the field editor and the new firstResponder is the window then the change in firstResponder is probably due to NSWindow's endEditingFor: method transferring firstResponder to the window temporarily before moving to the next view
	if (firstResponder == [self window] && previousFirstResponder == [[self window] fieldEditor:NO forObject:nil])
		return;
	// Otherwise the firstResponder has moved away from self or its subviews, so stop editing
	[self stopEditing];
	[self setNeedsDisplay:YES];
}

-(void)stopEditing {
	if (![self editingElement])
		return;
	ALTimelineElement *cachedElement = [self editingElement];
	[self setEditingElement:nil];
	[[self window] removeObserver:self forKeyPath:@"firstResponder"];
	NSResponder *firstResponder = [[self window] firstResponder];
	if ([firstResponder isKindOfClass:[NSView class]] && [(NSView *)firstResponder isDescendantOf:self])
		[[self window] makeFirstResponder:[self canvas]];
	[cachedElement timelineElementDidEndEditing];
}

// Point is in the given element view's coordinates
-(BOOL)editElement:(ALTimelineElement *)element atPoint:(NSPoint)clickPoint {
	if ([[self delegate] respondsToSelector:@selector(timeline:shouldEditElement:)] && ![[self delegate] timeline:self shouldEditElement:element])
		return NO;
	[self stopEditing];
	[self scrollElementToVisible:element];
	[self setEditingElement:element];
	[[self window] addObserver:self forKeyPath:@"firstResponder" options:NSKeyValueObservingOptionOld context:NULL];
	[element timelineElementDidBeginEditingAtPoint:clickPoint];
	return YES;
}

@end
